#!/usr/bin/env node
let fs = require('fs')
let path = require('path')
let api = require('../src/api.json')

/**
 * Utilities & helpers
 */

function capitalize(str) {
  return str[0].toUpperCase() + str.slice(1)
}

function trim(str) {
  return str.trim()
}

function dedent(str) {
  return str.split("\n").map(trim).join("\n")
}

function flatten(arrays) {
  return [].concat(...arrays)
}

function keys(obj) {
  return Object.keys(obj)
}

/**
 * Code gen utilities.
 */

function Namespace(name, exports) {
  return (`
    declare namespace ${name} {
      ${exports.map(Export).join('\n\n')}
    }
  `).trim()
}

function Export(code) {
  return `export ${code}`
}

function Interface(name, props) {
  return (`
    interface ${name} {
      ${props.map(KeyValue).join("\n")}
    }
  `).trim()
}

function ObjectLiteral(props) {
  return (`
    {
      ${props.map(KeyValue).join("\n")}
    }
  `).trim()
}

function KeyValue(kv) {
  return `${kv.key}: ${kv.value}`
}

function Module(name, lets) {
  return (`
    declare module "${name}" {
      ${lets.map(Export).join('\n\n')}
    }
  `).trim()
}

function Let(name, type) {
  return `let ${name}: ${type}`
}

function File(expressions) {
  return (`
    // generated from scripts/generate-types

    ${expressions.join("\n\n")}
  `).trim()
}

function Generic(name, t) {
  return `${name}<${t}>`
}

function Method(name, args, type) {
  return {
    key: `${name}(${args.join(", ")})`,
    value: type
  }
}

function Property(name, value) {
  return {
    key: name,
    value: value
  }
}

function Argument(name, type) {
  return `${name}: ${type}`
}

/**
 * Converts the object format in api.json into a properly nested tree
 * that can be recursed through to generate the source for objects
 * with nested methods.
 */
function apiToTree() {
  let namespaces = {}

  for (let method in api) {
    let parts = method.split('.')
    let params = api[method]
    let namespace = namespaces

    let node = {
      params: params,
      name: parts.map(capitalize).join("."),
      func: parts[parts.length - 1]
    }

    while (parts.length) {
      let key = parts.shift()

      if (parts.length) {
        namespace = (namespace[key] = namespace[key] || {})
      } else {
        namespace[key] = node
      }
    }
  }

  return namespaces
}

/**
 * Quick and dirty helper for getting passable formatting and indentation
 * from a block of code based on curly brace locations.
 */
function format(code) {
  let indent = 0
  let unformatted = dedent(code)
  let formatted = []

  for (let i = 0; i < unformatted.length; i++) {
    let ch = unformatted[i]

    if (ch === "{") {
      indent += 1
    }

    if (ch === "}") {
      formatted.pop()
      indent -= 1
    }

    formatted.push(ch)

    if (ch === "\n") {
      for (let n = 0; n < indent; n++) {
        formatted.push("  ")
      }
    }
  }

  // Remove whitespace from lines with no other chars
  return formatted.join("").replace(/^\s+$/g, "")
}

/**
 * Recursive helper for generating properties and methods inside the
 * exported values.
 */
function types(value) {
  if ('params' in value) {
    return [
      Method(
        value.func,
        [Argument('params', `${value.name}.Params`)],
        Generic('Promise', `${value.name}.Response`)
      ),
      Method(
        value.func,
        [
          Argument('params', `${value.name}.Params`),
          Argument('callback', `(params: ${value.name}.Params) => void`),
        ],
        'void',
      ),
    ]
  }

  return flatten(keys(value).map(key => {
    let subtype = value[key]
    let props = types(subtype)

    if ('params' in subtype) {
      return props
    }

    return Property(key, ObjectLiteral(props))
  }))
}

function createDeclarations() {
  let tree = apiToTree()

  let namespaces = keys(api).map(method => {
    let name = method.split(".").map(capitalize).join(".")
    let params = api[method]

    return Namespace(name, [
      Interface('Params', params.map(param => {
        return { key: param, value: 'any' }
      }).concat([
        { key: '[optional: string]', value: 'any' }
      ])),
      Interface('Response', [
        { key: 'ok', value: 'boolean' },
        { key: '[key: string]', value: 'any' }
      ])
    ])
  })

  let exported = Module('slack', keys(tree).map(key => {
    let value = tree[key]
    let props = types(value)
    return Let(key, ObjectLiteral(props))
  }))

  return File(namespaces.concat(exported))
}

let outfile = path.resolve(__dirname, '../slack.d.ts')
let source = format(createDeclarations())

fs.writeFileSync(outfile, source)

